package org.apache.lucene;

import java.io.FileNotFoundException;
import java.io.IOException;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.CloseCallback;
import org.apache.lucene.index.IndexProviderIndexWriter;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.GAERAMDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Version;

import com.google.appengine.api.datastore.Key;

/**
 * This class allows you create a single
 * shared instance of GAERAMDirectory,
 * IndexSearcher, and IndexWriter.
 * 
 * Directory is instantiated during initialization.
 * 
 * 
 * Calls for the IndexSearcher will perform a check
 * to see if the time since the last update check
 * has exceeded the check threshold, if it has the
 * Directory will check for updates.
 * 
 * The IndexWriter will be passed a reference to the caller
 * (null will cause a IllegalArgumentException to be thrown).
 * The IndexWriter will not actually close itself until after
 * all the caller references have called close();
 * 
 * 
 * @author Joel Wilson
 *
 */
public class GAERAMIndexProvider implements IndexProvider {
	
	private final GAERAMDirectory directory;
	private IndexSearcher searcher;
	private IndexProviderIndexWriter writer = null;
	private long timeBetweenRefreshChecks = 1000 * 60 * 2;
	private long lastCheckTime = -1;
	
	private final CloseCallback closeCallback = new CloseCallback() {

		public synchronized void closed() {
			writer = null;
			try {
				updateSearcher();
			} 
			catch (IOException ioException) {}
			catch (RuntimeException runtimeException) {}
		}
	};
	
	public GAERAMIndexProvider(String directoryName) {
		directory = new GAERAMDirectory(directoryName, false);
	}
	
	public GAERAMIndexProvider(String directoryName, Key parentKey) {
		directory = new GAERAMDirectory(directoryName, false, parentKey);
	}
	
	public GAERAMIndexProvider(String directoryName, boolean preload, Key parentKey) {
		directory = new GAERAMDirectory(directoryName, preload, parentKey);
	}
	
	synchronized void checkIfUpdateTime() throws IOException {
		boolean needsUpdating = false;
		
		// check if we need to try updating
		if (lastCheckTime == -1)
			needsUpdating = true;
		else
			needsUpdating = System.currentTimeMillis() - lastCheckTime > timeBetweenRefreshChecks;
		
		// perform the update if warranted
		if (needsUpdating && directory.pullLatest())
			updateSearcher();
		else
			lastCheckTime = System.currentTimeMillis();
	}
	
	synchronized void updateSearcher() throws IOException {
		// determine if this works or not, we might need to replace the
		// searcher rather than just call reopen on it's IndexReader.
		if (searcher != null)
			searcher.getIndexReader().reopen();
		lastCheckTime = System.currentTimeMillis();
	}
	
	public Directory getDirectory() {
		return directory;
	}
	
	public IndexSearcher getIndexSearcher() throws IOException {
		checkIfUpdateTime();
		try {
		if (searcher == null)
			searcher = new IndexSearcher(getDirectory());
		} catch (FileNotFoundException fileNotFound) {
			IndexWriter writer = getStandardIndexWriter(0, 0);
			writer.close();
			
			searcher = new IndexSearcher(getDirectory());
		}
		
		return searcher;
	}
	
	public synchronized IndexWriter getStandardIndexWriter() throws IOException, LockObtainFailedException {
		return getStandardIndexWriter(3, 100); // default at 3 tries, with 100 millis initial delay
	}
	
	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public synchronized IndexWriter getStandardIndexWriter(int numberOfRetries, int initialDelayMillis) throws IOException, LockObtainFailedException {	
		if (writer == null) {
			int attempts = 1;
			int timeout = initialDelayMillis;
			while (true) {
				try {
					writer = new IndexProviderIndexWriter(getDirectory(), new StandardAnalyzer(Version.LUCENE_30), MaxFieldLength.LIMITED, closeCallback);
					break;
				} catch (LockObtainFailedException lockObtainFailedException) {
					// retry logic
					if (attempts == numberOfRetries) throw lockObtainFailedException;
					try {
						Thread.sleep(timeout);
					} catch (InterruptedException interruptedException) {}
					timeout *= 2;
					attempts ++;
				}
			}
			return writer;
		} else
			throw new LockObtainFailedException("Writer is already in use.");
	}

	public long getTimeBetweenRefreshChecks() {
		return timeBetweenRefreshChecks;
	}

	/**
	 * Allows you to set the time between refresh checks.
	 * Unit of measure is milliseconds, default is for 2 minutes.
	 * 
	 * Throws IllegalArgumentException if the value you pass in is
	 * <= 1 || > Long.MAX_VALUE
	 * 
	 * @param timeInMillis
	 */
	public synchronized void setTimeBetweenRefreshChecks(long timeInMillis) {
		if (timeInMillis > 1 && timeInMillis <= Long.MAX_VALUE)
			throw new IllegalArgumentException("Invalid time parameter passed in.");
		
		this.timeBetweenRefreshChecks = timeInMillis;
	}
	
	public synchronized void resetWriter() {
		this.writer = null;
	}
}
